-
Notifications
You must be signed in to change notification settings - Fork 175
Feat/fp #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/fp #74
Conversation
…ence methods - fix: distinguish params/payload parameters in sign_headers method - params: for GET requests only - payload: for POST requests only - feat: add sign_headers_get convenience method for GET requests - feat: add sign_headers_post convenience method for POST requests - docs: update docstrings with clear parameter usage examples
- fix: use params for GET and payload for POST in sign_headers tests - test: add test_sign_headers_get for GET convenience method - test: add test_sign_headers_post for POST convenience method - test: verify timestamp parameter support in convenience methods
- feat: promote sign_headers_get/post as recommended approach - feat: move build_url and build_json_body to recommended section - refactor: move sign_headers unified method to traditional methods section - refactor: collapse traditional single-field generation methods into details - improve: clearer documentation structure for better user experience
- validate GET requests only use params, not payload - validate POST requests only use payload, not params - raise ValueError for unsupported HTTP methods (only GET/POST allowed) - add comprehensive test coverage for parameter validation - improve error messages for better developer experience
…ad of the official standard base64 method. Add some initialization parameters to the configuration file to generate fingerprints. Perhaps using customer_encoder to replace the encoder file name is better than directly using the encoder name, since encoders are a native Python method.
…ad of the official standard base64 method. Add some initialization parameters to the configuration file to generate fingerprints. ADD CRC32_encrypt for gen xs-common Perhaps using customer_encoder to replace the encoder file name is better than directly using the encoder name, since encoders are a native Python method. Use case: cookie = "you cookie dict or string" xs_common = client.sign_xsc(cookie)
…byte corruption during encoding. This restores parity with browser/VM output: the base64 result must start with I38r…. Prevents downstream requests from being rejected due to an invalid b1 value.
Reviewer's Guide基于合成的浏览器指纹新增 x-s-common 签名流水线,重构 header 签名 API 以接受完整 cookies(而非仅 a1),引入 CRC32 和指纹生成功能工具以及相关配置常量,并修复/定制 Base64 编码逻辑,使其在原始字节和缓存转换表上工作,以贴近浏览器行为并提升性能。 带 x-s-common 生成流程的 sign_headers_get 时序图sequenceDiagram
actor Developer
participant Xhshow
participant RequestSignatureValidator as Validator
participant XsCommonSigner as CommonSigner
participant FingerprintGenerator as FpGen
participant CRC32
participant Base64Encoder as Encoder
Developer->>Xhshow: sign_headers_get(uri, cookies, xsec_appid, params, timestamp?)
activate Xhshow
Xhshow->>Xhshow: sign_headers("GET", uri, cookies, xsec_appid, params, None, timestamp)
Xhshow->>Xhshow: _parse_cookies(cookies)
note right of Xhshow: Accept dict or cookie string
Xhshow-->>Xhshow: cookie_dict
Xhshow->>Xhshow: a1_value = cookie_dict["a1"]
Xhshow->>XsCommonSigner: sign(cookie_dict)
activate CommonSigner
CommonSigner->>FpGen: generate(cookies=cookie_dict, user_agent=PUBLIC_USERAGENT)
activate FpGen
FpGen-->>CommonSigner: fingerprint dict
CommonSigner->>FpGen: generate_b1(fingerprint)
FpGen->>Encoder: encode(bytearray_from_encrypted_fp)
activate Encoder
Encoder-->>FpGen: b1 (custom base64)
deactivate Encoder
FpGen-->>CommonSigner: b1
deactivate FpGen
CommonSigner->>CRC32: crc32_js_int(b1)
activate CRC32
CRC32-->>CommonSigner: x9_crc32
deactivate CRC32
CommonSigner->>Encoder: encode(json.dumps(sign_struct))
activate Encoder
Encoder-->>CommonSigner: xs_common
deactivate Encoder
CommonSigner-->>Xhshow: xs_common
deactivate CommonSigner
Xhshow->>Xhshow: x_s = sign_xs("GET", uri, a1_value, xsec_appid, request_data, timestamp)
Xhshow->>Xhshow: x_t = get_x_t(timestamp)
Xhshow->>Xhshow: x_b3_traceid = get_b3_trace_id()
Xhshow->>Xhshow: x_xray_traceid = get_xray_trace_id()
Xhshow-->>Developer: headers {"x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"}
deactivate Xhshow
签名、指纹与编码组件的更新类图classDiagram
class Xhshow {
-config CryptoConfig
+sign_xs_common(cookie_dict: dict~str,Any~ | str) str
+sign_xsc(cookie_dict: dict~str,Any~ | str) str
+sign_headers(method: str, uri: str, cookies: dict~str,Any~ | str, xsec_appid: str, params: dict~str,Any~?, payload: dict~str,Any~?, timestamp: float?) dict~str,str~
+sign_headers_get(uri: str, cookies: dict~str,Any~ | str, xsec_appid: str, params: dict~str,Any~?, timestamp: float?) dict~str,str~
+sign_headers_post(uri: str, cookies: dict~str,Any~ | str, xsec_appid: str, payload: dict~str,Any~?, timestamp: float?) dict~str,str~
+sign_xs(method: str, uri: str, a1_value: str, xsec_appid: str, payload: dict~str,Any~?, timestamp: float?) str
+get_x_t(timestamp: float?) int
+get_b3_trace_id() str
+get_xray_trace_id(timestamp: int) str
-_parse_cookies(cookies: dict~str,Any~ | str) dict~str,Any~
}
class XsCommonSigner {
-config CryptoConfig
-_fp_generator FingerprintGenerator
-_encoder Base64Encoder
+XsCommonSigner(config: CryptoConfig?)
+sign(cookie_dict: dict~str,Any~) str
}
class FingerprintGenerator {
-config CryptoConfig
-_b1_key bytes
-_encoder Base64Encoder
+FingerprintGenerator(config: CryptoConfig)
+generate_b1(fp: dict~str,Any~) str
+generate(cookies: dict~str,Any~, user_agent: str) dict~str,Any~
+update(fp: dict~str,Any~, cookies: dict~str,Any~, url: str) void
}
class CRC32 {
<<static>> MASK32 int
<<static>> POLY int
<<static>> _TABLE list~int~?
<<static>> _ensure_table() void
<<static>> _crc32_core(data: DataLike, string_mode: str) int
<<static>> _to_signed32(u: int) int
<<static>> crc32_js_int(data: DataLike, string_mode: str, signed: bool) int
}
class Base64Encoder {
-config CryptoConfig
-_custom_encode_table dict
-_custom_decode_table dict
-_x3_encode_table dict
-_x3_decode_table dict
+Base64Encoder(config: CryptoConfig)
+encode(data_to_encode: bytes | str | Iterable~int~) str
+decode(encoded_string: str) str
+decode_x3(encoded_string: str) bytes
+encode_x3(input_bytes: bytes | bytearray) str
}
class CryptoConfig {
+DES_KEY str
+GID_URL str
+DATA_PALTFORM str
+DATA_SVN str
+DATA_SDK_VERSION str
+DATA_webBuild str
+MAX_32BIT int
+MAX_SIGNED_32BIT int
+STANDARD_BASE64_ALPHABET str
+CUSTOM_BASE64_ALPHABET str
+X3_BASE64_ALPHABET str
+X3_PREFIX str
+XYS_PREFIX str
+XRAY_TRACE_ID_PREFIX str
+XRAY_TRACE_ID_PART1_LENGTH int
+XRAY_TRACE_ID_PART2_LENGTH int
+B3_TRACE_ID_LENGTH int
+B1_SECRET_KEY str
+SIGNATURE_XSCOMMON_TEMPLATE dict~str,Any~
+PUBLIC_USERAGENT str
+with_overrides(**kwargs: Any) CryptoConfig
}
class RequestSignatureValidator {
+validate_payload(payload: Any) dict~str,Any~?
+validate_cookie(cookie: Any) dict~str,Any~ | str
}
class validate_xs_common_params {
<<function>>
}
Xhshow --> CryptoConfig : uses
Xhshow --> XsCommonSigner : creates
Xhshow --> RequestSignatureValidator : via validate_xs_common_params
XsCommonSigner --> FingerprintGenerator : composes
XsCommonSigner --> Base64Encoder : composes
XsCommonSigner --> CryptoConfig : uses
XsCommonSigner --> CRC32 : calls
FingerprintGenerator --> CryptoConfig : uses
FingerprintGenerator --> Base64Encoder : uses
Base64Encoder --> CryptoConfig : uses
CRC32 --> CryptoConfig : uses POLY constant
validate_xs_common_params --> RequestSignatureValidator : uses
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your Experience打开你的 dashboard 以:
Getting HelpOriginal review guide in EnglishReviewer's GuideAdds an x-s-common signing pipeline based on synthesized browser fingerprints, refactors header-signing APIs to accept full cookies instead of only a1, introduces CRC32 and fingerprint-generation utilities plus config constants, and fixes/customizes Base64 encoding to operate on raw bytes and cached translation tables for browser parity and performance. Sequence diagram for sign_headers_get with x-s-common generationsequenceDiagram
actor Developer
participant Xhshow
participant RequestSignatureValidator as Validator
participant XsCommonSigner as CommonSigner
participant FingerprintGenerator as FpGen
participant CRC32
participant Base64Encoder as Encoder
Developer->>Xhshow: sign_headers_get(uri, cookies, xsec_appid, params, timestamp?)
activate Xhshow
Xhshow->>Xhshow: sign_headers("GET", uri, cookies, xsec_appid, params, None, timestamp)
Xhshow->>Xhshow: _parse_cookies(cookies)
note right of Xhshow: Accept dict or cookie string
Xhshow-->>Xhshow: cookie_dict
Xhshow->>Xhshow: a1_value = cookie_dict["a1"]
Xhshow->>XsCommonSigner: sign(cookie_dict)
activate CommonSigner
CommonSigner->>FpGen: generate(cookies=cookie_dict, user_agent=PUBLIC_USERAGENT)
activate FpGen
FpGen-->>CommonSigner: fingerprint dict
CommonSigner->>FpGen: generate_b1(fingerprint)
FpGen->>Encoder: encode(bytearray_from_encrypted_fp)
activate Encoder
Encoder-->>FpGen: b1 (custom base64)
deactivate Encoder
FpGen-->>CommonSigner: b1
deactivate FpGen
CommonSigner->>CRC32: crc32_js_int(b1)
activate CRC32
CRC32-->>CommonSigner: x9_crc32
deactivate CRC32
CommonSigner->>Encoder: encode(json.dumps(sign_struct))
activate Encoder
Encoder-->>CommonSigner: xs_common
deactivate Encoder
CommonSigner-->>Xhshow: xs_common
deactivate CommonSigner
Xhshow->>Xhshow: x_s = sign_xs("GET", uri, a1_value, xsec_appid, request_data, timestamp)
Xhshow->>Xhshow: x_t = get_x_t(timestamp)
Xhshow->>Xhshow: x_b3_traceid = get_b3_trace_id()
Xhshow->>Xhshow: x_xray_traceid = get_xray_trace_id()
Xhshow-->>Developer: headers {"x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"}
deactivate Xhshow
Updated class diagram for signing, fingerprint, and encoding componentsclassDiagram
class Xhshow {
-config CryptoConfig
+sign_xs_common(cookie_dict: dict~str,Any~ | str) str
+sign_xsc(cookie_dict: dict~str,Any~ | str) str
+sign_headers(method: str, uri: str, cookies: dict~str,Any~ | str, xsec_appid: str, params: dict~str,Any~?, payload: dict~str,Any~?, timestamp: float?) dict~str,str~
+sign_headers_get(uri: str, cookies: dict~str,Any~ | str, xsec_appid: str, params: dict~str,Any~?, timestamp: float?) dict~str,str~
+sign_headers_post(uri: str, cookies: dict~str,Any~ | str, xsec_appid: str, payload: dict~str,Any~?, timestamp: float?) dict~str,str~
+sign_xs(method: str, uri: str, a1_value: str, xsec_appid: str, payload: dict~str,Any~?, timestamp: float?) str
+get_x_t(timestamp: float?) int
+get_b3_trace_id() str
+get_xray_trace_id(timestamp: int) str
-_parse_cookies(cookies: dict~str,Any~ | str) dict~str,Any~
}
class XsCommonSigner {
-config CryptoConfig
-_fp_generator FingerprintGenerator
-_encoder Base64Encoder
+XsCommonSigner(config: CryptoConfig?)
+sign(cookie_dict: dict~str,Any~) str
}
class FingerprintGenerator {
-config CryptoConfig
-_b1_key bytes
-_encoder Base64Encoder
+FingerprintGenerator(config: CryptoConfig)
+generate_b1(fp: dict~str,Any~) str
+generate(cookies: dict~str,Any~, user_agent: str) dict~str,Any~
+update(fp: dict~str,Any~, cookies: dict~str,Any~, url: str) void
}
class CRC32 {
<<static>> MASK32 int
<<static>> POLY int
<<static>> _TABLE list~int~?
<<static>> _ensure_table() void
<<static>> _crc32_core(data: DataLike, string_mode: str) int
<<static>> _to_signed32(u: int) int
<<static>> crc32_js_int(data: DataLike, string_mode: str, signed: bool) int
}
class Base64Encoder {
-config CryptoConfig
-_custom_encode_table dict
-_custom_decode_table dict
-_x3_encode_table dict
-_x3_decode_table dict
+Base64Encoder(config: CryptoConfig)
+encode(data_to_encode: bytes | str | Iterable~int~) str
+decode(encoded_string: str) str
+decode_x3(encoded_string: str) bytes
+encode_x3(input_bytes: bytes | bytearray) str
}
class CryptoConfig {
+DES_KEY str
+GID_URL str
+DATA_PALTFORM str
+DATA_SVN str
+DATA_SDK_VERSION str
+DATA_webBuild str
+MAX_32BIT int
+MAX_SIGNED_32BIT int
+STANDARD_BASE64_ALPHABET str
+CUSTOM_BASE64_ALPHABET str
+X3_BASE64_ALPHABET str
+X3_PREFIX str
+XYS_PREFIX str
+XRAY_TRACE_ID_PREFIX str
+XRAY_TRACE_ID_PART1_LENGTH int
+XRAY_TRACE_ID_PART2_LENGTH int
+B3_TRACE_ID_LENGTH int
+B1_SECRET_KEY str
+SIGNATURE_XSCOMMON_TEMPLATE dict~str,Any~
+PUBLIC_USERAGENT str
+with_overrides(**kwargs: Any) CryptoConfig
}
class RequestSignatureValidator {
+validate_payload(payload: Any) dict~str,Any~?
+validate_cookie(cookie: Any) dict~str,Any~ | str
}
class validate_xs_common_params {
<<function>>
}
Xhshow --> CryptoConfig : uses
Xhshow --> XsCommonSigner : creates
Xhshow --> RequestSignatureValidator : via validate_xs_common_params
XsCommonSigner --> FingerprintGenerator : composes
XsCommonSigner --> Base64Encoder : composes
XsCommonSigner --> CryptoConfig : uses
XsCommonSigner --> CRC32 : calls
FingerprintGenerator --> CryptoConfig : uses
FingerprintGenerator --> Base64Encoder : uses
Base64Encoder --> CryptoConfig : uses
CRC32 --> CryptoConfig : uses POLY constant
validate_xs_common_params --> RequestSignatureValidator : uses
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there - I've reviewed your changes and found some issues that need to be addressed.
- Base64Encoder.encode 现在声称支持 bytes 和 Iterable[int],但它仍然在任何非 bytearray 输入上无条件调用 .encode('utf-8'),这会导致对 bytes/iterable 输入报错,并且对 bytes 和 bytearray 的处理不一致;建议显式规范化输入类型(例如分别处理 str、bytes/bytearray/memoryview 和 Iterable[int])。
- CRC32._crc32_core 使用了
isinstance(data, bytes | bytearray | memoryview),但在运行时isinstance不支持 PEP 604 形式的 union;请改为使用元组(例如(bytes, bytearray, memoryview))以避免 TypeError。 - RequestSignatureValidator.validate_cookie 里的错误信息似乎是从其它地方直接复制过来的,仍然提到 “payload” 和 “payload keys”,这在 cookie 校验场景中具有误导性;请将这些消息更新为引用 “cookie”,以提供更清晰的诊断信息。
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Base64Encoder.encode now advertises support for bytes and Iterable[int], but it still unconditionally calls .encode('utf-8') on any non-bytearray input, which will fail for bytes/iterables and also treats bytes differently from bytearray; consider normalizing input types explicitly (e.g., separate branches for str, bytes/bytearray/memoryview, and Iterable[int]).
- CRC32._crc32_core uses `isinstance(data, bytes | bytearray | memoryview)`, but `isinstance` does not accept PEP 604 unions at runtime; replace this with a tuple (e.g., `(bytes, bytearray, memoryview)`) to avoid a TypeError.
- RequestSignatureValidator.validate_cookie appears to have copy-pasted error messages mentioning 'payload' and 'payload keys', which is misleading for cookie validation; update these messages to refer to 'cookie' instead for clearer diagnostics.
## Individual Comments
### Comment 1
<location> `src/xhshow/utils/encoder.py:33-36` </location>
<code_context>
+ )
- def encode(self, data_to_encode: str) -> str:
+ def encode(self, data_to_encode: bytes | str | Iterable[int]) -> str:
"""
Encode a string using custom Base64 alphabet
</code_context>
<issue_to_address>
**issue (bug_risk):** 新的 `encode` 函数签名与实现不一致,会在处理 `bytes` 和可迭代输入时出错。
新的类型标注允许 `bytes` 和 `Iterable[int]`,但在非 `bytearray` 分支中实现仍然假定参数为 `str`。`data_bytes = data_to_encode` 之后再调用 `data_to_encode.encode("utf-8")`,对于 `bytes` 和通用可迭代对象都会失败。
要么让实现与注解保持一致(例如先把所有输入统一规范化为 `bytes`,再进行编码/转换),要么如果不需要更宽的支持,就把类型标注收窄回 `str`。
</issue_to_address>
### Comment 2
<location> `src/xhshow/core/crc32_encrypt.py:66` </location>
<code_context>
+
+ c = cls.MASK32
+
+ if isinstance(data, bytes | bytearray | memoryview):
+ it = bytes(data)
+ elif isinstance(data, str):
</code_context>
<issue_to_address>
**issue (bug_risk):** 使用 `|` union 的 `isinstance` 检查会在运行时抛异常,应该改为使用类型元组。
在 CPython 中,`isinstance(obj, T1 | T2)` 会抛出 `TypeError: isinstance() argument 2 cannot be a union`。为了在不引入运行时错误的前提下保持相同行为,应修改为:
```python
if isinstance(data, (bytes, bytearray, memoryview)):
it = bytes(data)
```
</issue_to_address>
### Comment 3
<location> `src/xhshow/utils/validators.py:80-90` </location>
<code_context>
return payload
+ @staticmethod
+ def validate_cookie(cookie: Any) -> dict[str, Any] | str:
+ """Validate cookie parameter"""
+ if cookie is not None and not (isinstance(cookie, dict) or isinstance(cookie, str)):
+ raise TypeError(f"payload must be dict or None, got {type(cookie).__name__}")
+ # detect cookie dict validation
+ if cookie is not None and isinstance(cookie, dict):
+ for key in cookie.keys():
+ if not isinstance(key, str):
+ raise TypeError(f"payload keys must be str, got {type(key).__name__} for key '{key}'")
+ return cookie
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** `validate_cookie` 中的错误信息引用了 `payload` 而不是 `cookie`,并且可接受类型与装饰器的用法不匹配。
当前抛出的异常消息提到 `payload`,并声称只接受 `dict or None`,这对一个 cookie 校验函数来说具有误导性,也会让调试变得困难。
此外还存在类型不匹配:`validate_xs_common_params` 可以传入 `None`(通过默认值),但 `validate_cookie` 的返回类型注解不包含 `None`。建议在函数签名中允许 `None`,并对错误信息和类型进行对齐,例如:
```python
def validate_cookie(cookie: Any) -> dict[str, Any] | str | None:
if cookie is not None and not isinstance(cookie, (dict, str)):
raise TypeError(f"cookie must be dict, str, or None, got {type(cookie).__name__}")
if isinstance(cookie, dict):
for key in cookie.keys():
if not isinstance(key, str):
raise TypeError(
f"cookie keys must be str, got {type(key).__name__} for key '{key}'"
)
return cookie
```
```suggestion
@staticmethod
def validate_cookie(cookie: Any) -> dict[str, Any] | str | None:
"""Validate cookie parameter"""
if cookie is not None and not isinstance(cookie, (dict, str)):
raise TypeError(
f"cookie must be dict, str, or None, got {type(cookie).__name__}"
)
if isinstance(cookie, dict):
for key in cookie.keys():
if not isinstance(key, str):
raise TypeError(
f"cookie keys must be str, got {type(key).__name__} for key '{key}'"
)
return cookie
```
</issue_to_address>
### Comment 4
<location> `src/xhshow/utils/validators.py:213-222` </location>
<code_context>
return wrapper # type: ignore
+
+
+def validate_xs_common_params(func: F) -> F: # type: ignore[misc] # noqa: UP047
+ """
+ Parameter validation decorator for the `sign_xsc` method.
+
+ This wrapper normalizes and validates the arguments before delegating to
+ the underlying signing implementation.
+
+ Args:
+ func: Method to be decorated.
+
+ Returns:
+ Wrapped method with validated parameters.
+ """
+
+ @wraps(func)
+ def wrapper(
+ self,
+ cookie_dict: dict[str, Any] | None = None,
+ ) -> str:
+ validator = RequestSignatureValidator()
</code_context>
<issue_to_address>
**issue (bug_risk):** `validate_xs_common_params` 更改了 `sign_xsc` 的函数签名并允许传入 `None`,这会在下游实现中导致问题。
当前包装器将 `sign_xsc` 的契约从 `dict[str, Any] | str` 改成了 `dict[str, Any] | None`,并丢失了 `str` 这种调用形式。
这会引发两个问题:
1. 传入字符串 cookie 的调用方不再受支持(或者类型标注变得不正确),因为按当前写法 `validate_cookie` 并不接受 `str | None`。
2. 当 `cookie_dict` 为 `None` 时,该值会一路传递到 `sign_xsc` → `_parse_cookies` → `XsCommonSigner.sign`,而最后这个方法期望得到 `dict` 并执行 `cookie_dict["a1"]`,从而导致运行时抛出 `TypeError`,而不是给出一致的校验错误。
为了解决这个问题,应保持包装器签名与 `sign_xsc` 一致,并确保 `validate_cookie` 支持 `str` 且对于必填参数拒绝 `None`:
```python
def wrapper(self, cookie_dict: dict[str, Any] | str) -> str:
validator = RequestSignatureValidator()
validated_cookie = validator.validate_cookie(cookie_dict)
if validated_cookie is None:
raise ValueError("cookie_dict is required")
return func(self, validated_cookie)
```
</issue_to_address>
### Comment 5
<location> `tests/test_crypto.py:529` </location>
<code_context>
+ )
+
+ assert isinstance(headers, dict)
+ assert "x-s" in headers
+ assert "x-s-common" in headers
+ assert "x-t" in headers
</code_context>
<issue_to_address>
**issue (testing):** 测试期望 `sign_headers` 输出中包含 `x-s`,但实现当前只返回 `x-s-common`。
`Xhshow.sign_headers` 的实现现在省略了 `"x-s"`,只返回 `"x-s-common"`、`"x-t"`、`"x-b3-traceid"` 和 `"x-xray-traceid"`,但这一处以及其它测试仍然断言存在 `"x-s"` 及其前缀。请先确定 `sign_headers*` 的对外契约究竟应该是什么,然后更新实现(重新暴露 `"x-s"`)或更新测试(不再断言它),以保证两者保持一致。
</issue_to_address>
### Comment 6
<location> `tests/test_crypto.py:584` </location>
<code_context>
+ )
+
+ assert isinstance(headers, dict)
+ assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"])
+ assert headers["x-s"].startswith("XYS_")
+ assert len(headers["x-s-common"]) > 0
</code_context>
<issue_to_address>
**issue (testing):** `sign_headers_get`/`sign_headers_post` 的测试仍然断言 `x-s` 的存在和格式,但该字段已从 headers 字典中移除。
这些测试仍然期望 `"x-s"` 存在并以 `"XYS_"` 开头,但 `client.sign_headers` 已不再返回这个 header。一旦确认期望的 header 集合,请只针对属于公开 API 的键进行断言。如果不再返回 `x-s`,请移除这里对 `XYS_` 前缀的断言,或者把它移动到直接针对底层 `sign_xs*` 帮助函数的测试中。
</issue_to_address>
### Comment 7
<location> `tests/test_cookie_parsing.py:214` </location>
<code_context>
+ )
+
+ assert isinstance(headers, dict)
+ assert "x-s" in headers
+ assert "x-s-common" in headers
+ assert "x-t" in headers
</code_context>
<issue_to_address>
**issue (testing):** 新增的 cookie/header 测试对 header 集合中的 `x-s` 进行了硬编码,这与当前实现不符。
这些测试目前断言 `sign_headers*` 的结果中存在 `"x-s"`,并在某些情况下断言它以 `"XYS_"` 开头,但本 PR 已经将 `sign_headers` 修改为仅返回 `x-s-common`(以及 trace ID),不再返回 `x-s`。请根据预期 API 进行对齐:
- 如果 `sign_headers*` 应该同时返回 `x-s` 和 `x-s-common`,请在实现中恢复 `x-s`。
- 如果只有 `x-s-common` 是对外暴露的字段,请删除这里对 `x-s` 的断言,并把任何关于 `x-s` 格式的检查迁移到更底层的签名函数测试中。
</issue_to_address>
### Comment 8
<location> `tests/test_cookie_parsing.py:135-96` </location>
<code_context>
+ with pytest.raises(KeyError, match="a1"):
+ self.client.sign_xs_common(cookies)
+
+ def test_sign_xsc_alias(self):
+ """测试 sign_xsc 别名方法"""
+ cookies = {
+ "a1": "test_a1_value",
+ "web_session": "test_session",
+ }
+
+ result = self.client.sign_xsc(cookies)
+
+ assert isinstance(result, str)
+ assert len(result) > 0
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** 为 `sign_xsc` / `validate_xs_common_params` 增加负向测试,覆盖类型和值校验逻辑。
由于 `sign_xsc` 使用了 `@validate_xs_common_params` 装饰器(内部会调用 `RequestSignatureValidator.validate_cookie`),当前测试只覆盖了传入合法字典的情况。请增加以下负向测试:
- 传入不受支持的类型(例如 list/int 作为 `cookie_dict`),并断言抛出带有预期消息的 `TypeError`。
- 使用包含非字符串键的 cookie 字典,以覆盖键类型校验路径。
- (可选)传入 `None`(取决于预期契约),并断言预期的行为。
这将帮助验证校验器的错误处理逻辑,并固定 `sign_xsc` 的参数契约。
建议实现:
```python
+ def test_sign_xsc_alias(self):
+ """测试 sign_xsc 别名方法"""
+ cookies = {
+ "a1": "test_a1_value",
+ "web_session": "test_session",
+ }
+
+ result = self.client.sign_xsc(cookies)
+
+ assert isinstance(result, str)
+ assert len(result) > 0
+
+ def test_sign_xsc_invalid_type(self):
+ """测试 sign_xsc 传入非 dict 类型参数时抛出异常"""
+ # list 作为参数
+ with pytest.raises(TypeError, match="cookie_dict"):
+ self.client.sign_xsc(["not", "a", "dict"]) # type: ignore[arg-type]
+
+ # int 作为参数
+ with pytest.raises(TypeError, match="cookie_dict"):
+ self.client.sign_xsc(123) # type: ignore[arg-type]
+
+ def test_sign_xsc_non_string_keys(self):
+ """测试 sign_xsc 传入非字符串 key 的 cookie 时抛出异常"""
+ cookies = {
+ "a1": "test_a1_value",
+ "web_session": "test_session",
+ 123: "value_with_int_key", # 非字符串 key
+ }
+
+ with pytest.raises(TypeError, match="key"):
+ self.client.sign_xsc(cookies) # type: ignore[dict-item]
+
+ def test_sign_xsc_none_cookie_dict(self):
+ """测试 sign_xsc 传入 None 时的参数校验行为"""
+ with pytest.raises(TypeError, match="cookie_dict"):
+ self.client.sign_xsc(None) # type: ignore[arg-type]
+
+
```
这些测试假设 `@validate_xs_common_params` / `RequestSignatureValidator.validate_cookie`:
1. 在 `cookie_dict` 不是 `dict`(list/int/None 情况)时抛出 `TypeError`,并且错误信息包含 `cookie_dict`。
2. 在存在非字符串键时抛出 `TypeError`,并且错误信息包含 `key`。
如果实际的校验器使用了不同的异常类型或消息,请相应地调整:
- `pytest.raises(...)` 中的异常类型;
- `match` 模式,以与真实错误消息保持一致。
如果测试类中已经提供了 `self.client.sign_xsc`,则不需要额外的代码变更。
</issue_to_address>
### Comment 9
<location> `tests/test_cookie_parsing.py:148-157` </location>
<code_context>
+class TestXsCommonSigner:
</code_context>
<issue_to_address>
**suggestion (testing):** 强化 `XsCommonSigner` 的测试,用固定向量或结构性断言覆盖 b1/Base64 回归问题。
当前测试只验证输出非空以及缺少 `a1` 时是否抛出 `KeyError`,并没有真正验证修复后的基于字节的 b1 Base64 行为。
为了更好地覆盖此次回归修复,建议增加一个确定性的测试:
- 使用固定的 cookie 字典和 `CryptoConfig`(具有确定性的 fingerprint),
- 通过 `FingerprintGenerator.generate_b1()` 或 `XsCommonSigner` 生成 `b1`,
- 断言 `b1`(或 `x-s-common`)与参考浏览器/VM 输出的已知正确值或前缀匹配(例如以 `"I38r"` 开头)。
建议实现:
```python
class TestXsCommonSigner:
"""测试 XsCommonSigner 类"""
def setup_method(self):
self.signer = XsCommonSigner()
def test_sign_with_dict_only(self):
"""测试只接受字典参数"""
cookies = {
"a1": "test_a1_value",
"web_session": "test_session",
}
# 基本行为:返回非空字符串
result = self.signer.sign(cookies)
assert isinstance(result, str)
assert result
def test_b1_base64_regression_vector(self):
"""使用固定向量验证 b1/Base64 行为,防止回归"""
cookies = {
"a1": "fixed_a1_value",
"web_session": "fixed_session_value",
}
# 使用固定的 CryptoConfig 以获得确定性的指纹
crypto_config = CryptoConfig(
fingerprint="fixed_fingerprint_for_test",
)
fp_gen = FingerprintGenerator(crypto_config=crypto_config)
# 直接生成 b1,确保是稳定的 Base64 字节编码
b1 = fp_gen.generate_b1(cookies)
assert isinstance(b1, str)
assert b1 # 非空
# 结构性断言:b1 必须以已知前缀开头(来自参考浏览器/VM 输出)
# 注意:前缀 "I38r" 需要根据实际参考输出进行同步
assert b1.startswith("I38r")
# 通过 XsCommonSigner 生成 x-s-common 头部,应与 b1 行为一致
x_s_common = self.signer.sign(cookies=cookies, crypto_config=crypto_config)
assert isinstance(x_s_common, str)
assert x_s_common
assert x_s_common.startswith("I38r")
```
1. 在 `tests/test_cookie_parsing.py` 顶部增加正确的导入(根据实际模块路径调整):
```python
from your_module_path import CryptoConfig, FingerprintGenerator
```
或者如果它们在不同模块中,请分别导入。
2. 确认 `XsCommonSigner` 的 API:
- 如果当前签名方法不是 `sign`,而是例如 `sign_xsc` 或其他名字,请将
```python
result = self.signer.sign(cookies)
x_s_common = self.signer.sign(cookies=cookies, crypto_config=crypto_config)
```
替换为实际的方法名和参数签名。
3. 如果 `CryptoConfig` 构造函数需要额外参数(例如密钥、盐值等),为测试构造一个完全确定性的配置(固定密钥/盐),并在 `test_b1_base64_regression_vector` 中补全这些参数。
4. `FingerprintGenerator` 的初始化和 `generate_b1` 调用可能需要不同的参数名或额外上下文:
- 将
```python
fp_gen = FingerprintGenerator(crypto_config=crypto_config)
b1 = fp_gen.generate_b1(cookies)
```
调整为当前项目中实际的初始化方式与方法签名。
5. `"I38r"` 仅是示例前缀,请从参考浏览器/VM 输出中获取真实的 `b1` 值或前缀,并更新断言:
```python
assert b1.startswith("<真实前缀>")
assert x_s_common.startswith("<真实前缀>")
```
6. 如项目中已有 pytest fixture 提供 `CryptoConfig` 或 `FingerprintGenerator`(例如 `crypto_config`、`fingerprint_generator`),可以将测试改写为使用这些 fixture,而不是在测试中直接构造实例,以保持与现有测试风格一致。
</issue_to_address>
### Comment 10
<location> `README.md:193` </location>
<code_context>
+cookie_string = "a1=your_a1_value; web_session=your_web_session; webId=your_web_id"
+xs_common = client.sign_xsc(cookie_dict=cookie_string)
+
+# 使用在请求中
+headers = {
+ "x-s-common": xs_common,
</code_context>
<issue_to_address>
**suggestion (typo):** “使用在请求中”建议调整为更自然的表达,如“在请求中使用”。
这句注释略显别扭,建议改为“# 在请求中使用”,更符合中文表达习惯。
```suggestion
# 在请求中使用
```
</issue_to_address>Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Original comment in English
Hey there - I've reviewed your changes and found some issues that need to be addressed.
- Base64Encoder.encode now advertises support for bytes and Iterable[int], but it still unconditionally calls .encode('utf-8') on any non-bytearray input, which will fail for bytes/iterables and also treats bytes differently from bytearray; consider normalizing input types explicitly (e.g., separate branches for str, bytes/bytearray/memoryview, and Iterable[int]).
- CRC32._crc32_core uses
isinstance(data, bytes | bytearray | memoryview), butisinstancedoes not accept PEP 604 unions at runtime; replace this with a tuple (e.g.,(bytes, bytearray, memoryview)) to avoid a TypeError. - RequestSignatureValidator.validate_cookie appears to have copy-pasted error messages mentioning 'payload' and 'payload keys', which is misleading for cookie validation; update these messages to refer to 'cookie' instead for clearer diagnostics.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Base64Encoder.encode now advertises support for bytes and Iterable[int], but it still unconditionally calls .encode('utf-8') on any non-bytearray input, which will fail for bytes/iterables and also treats bytes differently from bytearray; consider normalizing input types explicitly (e.g., separate branches for str, bytes/bytearray/memoryview, and Iterable[int]).
- CRC32._crc32_core uses `isinstance(data, bytes | bytearray | memoryview)`, but `isinstance` does not accept PEP 604 unions at runtime; replace this with a tuple (e.g., `(bytes, bytearray, memoryview)`) to avoid a TypeError.
- RequestSignatureValidator.validate_cookie appears to have copy-pasted error messages mentioning 'payload' and 'payload keys', which is misleading for cookie validation; update these messages to refer to 'cookie' instead for clearer diagnostics.
## Individual Comments
### Comment 1
<location> `src/xhshow/utils/encoder.py:33-36` </location>
<code_context>
+ )
- def encode(self, data_to_encode: str) -> str:
+ def encode(self, data_to_encode: bytes | str | Iterable[int]) -> str:
"""
Encode a string using custom Base64 alphabet
</code_context>
<issue_to_address>
**issue (bug_risk):** The new `encode` signature and implementation are inconsistent and will break for `bytes` and iterable inputs.
The new type hint allows `bytes` and `Iterable[int]`, but the implementation still assumes a `str` in the non-`bytearray` branch. `data_bytes = data_to_encode` followed by `data_to_encode.encode("utf-8")` will fail for `bytes` and generic iterables.
Either align the implementation with the annotation (e.g. normalize all inputs to `bytes` first, then encode/translate) or narrow the annotation back to `str` if wider support isn’t required.
</issue_to_address>
### Comment 2
<location> `src/xhshow/core/crc32_encrypt.py:66` </location>
<code_context>
+
+ c = cls.MASK32
+
+ if isinstance(data, bytes | bytearray | memoryview):
+ it = bytes(data)
+ elif isinstance(data, str):
</code_context>
<issue_to_address>
**issue (bug_risk):** The `isinstance` check using a `|` union will raise at runtime; it should use a tuple of types instead.
In CPython, `isinstance(obj, T1 | T2)` raises `TypeError: isinstance() argument 2 cannot be a union`. To keep the same behavior without a runtime error, this should be:
```python
if isinstance(data, (bytes, bytearray, memoryview)):
it = bytes(data)
```
</issue_to_address>
### Comment 3
<location> `src/xhshow/utils/validators.py:80-90` </location>
<code_context>
return payload
+ @staticmethod
+ def validate_cookie(cookie: Any) -> dict[str, Any] | str:
+ """Validate cookie parameter"""
+ if cookie is not None and not (isinstance(cookie, dict) or isinstance(cookie, str)):
+ raise TypeError(f"payload must be dict or None, got {type(cookie).__name__}")
+ # detect cookie dict validation
+ if cookie is not None and isinstance(cookie, dict):
+ for key in cookie.keys():
+ if not isinstance(key, str):
+ raise TypeError(f"payload keys must be str, got {type(key).__name__} for key '{key}'")
+ return cookie
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The error messages in `validate_cookie` reference `payload` instead of `cookie` and the accepted types don’t match the decorator usage.
The exceptions currently mention `payload` and refer to `dict or None`, which is misleading for a cookie validator and will confuse debugging.
There’s also a type mismatch: `validate_xs_common_params` can pass `None` (via the default), but `validate_cookie`’s return annotation doesn’t include `None`. Consider allowing `None` in the signature and aligning the error messages and types, e.g.:
```python
def validate_cookie(cookie: Any) -> dict[str, Any] | str | None:
if cookie is not None and not isinstance(cookie, (dict, str)):
raise TypeError(f"cookie must be dict, str, or None, got {type(cookie).__name__}")
if isinstance(cookie, dict):
for key in cookie.keys():
if not isinstance(key, str):
raise TypeError(
f"cookie keys must be str, got {type(key).__name__} for key '{key}'"
)
return cookie
```
```suggestion
@staticmethod
def validate_cookie(cookie: Any) -> dict[str, Any] | str | None:
"""Validate cookie parameter"""
if cookie is not None and not isinstance(cookie, (dict, str)):
raise TypeError(
f"cookie must be dict, str, or None, got {type(cookie).__name__}"
)
if isinstance(cookie, dict):
for key in cookie.keys():
if not isinstance(key, str):
raise TypeError(
f"cookie keys must be str, got {type(key).__name__} for key '{key}'"
)
return cookie
```
</issue_to_address>
### Comment 4
<location> `src/xhshow/utils/validators.py:213-222` </location>
<code_context>
return wrapper # type: ignore
+
+
+def validate_xs_common_params(func: F) -> F: # type: ignore[misc] # noqa: UP047
+ """
+ Parameter validation decorator for the `sign_xsc` method.
+
+ This wrapper normalizes and validates the arguments before delegating to
+ the underlying signing implementation.
+
+ Args:
+ func: Method to be decorated.
+
+ Returns:
+ Wrapped method with validated parameters.
+ """
+
+ @wraps(func)
+ def wrapper(
+ self,
+ cookie_dict: dict[str, Any] | None = None,
+ ) -> str:
+ validator = RequestSignatureValidator()
</code_context>
<issue_to_address>
**issue (bug_risk):** `validate_xs_common_params` changes the `sign_xsc` signature and allows `None`, which will break in the downstream implementation.
The wrapper currently changes the contract of `sign_xsc` from `dict[str, Any] | str` to `dict[str, Any] | None` and drops the `str` case.
This causes two problems:
1. Callers passing a cookie string are no longer supported (or the types are wrong), since `validate_cookie` doesn’t accept `str | None` as written.
2. When `cookie_dict` is `None`, `None` propagates into `sign_xsc` → `_parse_cookies` → `XsCommonSigner.sign`, which expects a `dict` and does `cookie_dict["a1"]`, leading to a runtime `TypeError` instead of consistent validation errors.
To fix this, keep the wrapper signature aligned with `sign_xsc` and ensure `validate_cookie` supports `str` and rejects `None` for required parameters:
```python
def wrapper(self, cookie_dict: dict[str, Any] | str) -> str:
validator = RequestSignatureValidator()
validated_cookie = validator.validate_cookie(cookie_dict)
if validated_cookie is None:
raise ValueError("cookie_dict is required")
return func(self, validated_cookie)
```
</issue_to_address>
### Comment 5
<location> `tests/test_crypto.py:529` </location>
<code_context>
+ )
+
+ assert isinstance(headers, dict)
+ assert "x-s" in headers
+ assert "x-s-common" in headers
+ assert "x-t" in headers
</code_context>
<issue_to_address>
**issue (testing):** Tests expect `x-s` in `sign_headers` output but implementation currently only returns `x-s-common`
The implementation of `Xhshow.sign_headers` now omits `"x-s"` and only returns `"x-s-common"`, `"x-t"`, `"x-b3-traceid"`, and `"x-xray-traceid"`, but this and other tests still assert `"x-s"` and its prefix. Please decide what the contract of `sign_headers*` should be and update either the implementation (to re‑expose `"x-s"`) or the tests (to stop asserting it) so they are consistent.
</issue_to_address>
### Comment 6
<location> `tests/test_crypto.py:584` </location>
<code_context>
+ )
+
+ assert isinstance(headers, dict)
+ assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"])
+ assert headers["x-s"].startswith("XYS_")
+ assert len(headers["x-s-common"]) > 0
</code_context>
<issue_to_address>
**issue (testing):** `sign_headers_get`/`sign_headers_post` tests still assert presence and format of `x-s` despite it being removed from the headers dict
These tests still expect `"x-s"` to be present and to start with `"XYS_"`, but `client.sign_headers` no longer returns that header. Once the expected header set is confirmed, please update them to only assert keys that are part of the public API. If `x-s` is no longer returned, remove the `XYS_` prefix assertion here or move it into a test that directly targets the lower-level `sign_xs*` helpers.
</issue_to_address>
### Comment 7
<location> `tests/test_cookie_parsing.py:214` </location>
<code_context>
+ )
+
+ assert isinstance(headers, dict)
+ assert "x-s" in headers
+ assert "x-s-common" in headers
+ assert "x-t" in headers
</code_context>
<issue_to_address>
**issue (testing):** New cookie/header tests hard-code `x-s` in the header set, which conflicts with the current implementation
These tests currently assert that `"x-s"` is present in `sign_headers*` results and sometimes that it starts with `"XYS_"`, but this PR changes `sign_headers` to return `x-s-common` (and trace IDs) without `x-s`. Please align the expectations with the intended API:
- If `sign_headers*` should return both `x-s` and `x-s-common`, restore `x-s` in the implementation.
- If only `x-s-common` is public, drop the `x-s` assertions here and move any `x-s` format checks into lower-level signing tests.
</issue_to_address>
### Comment 8
<location> `tests/test_cookie_parsing.py:135-96` </location>
<code_context>
+ with pytest.raises(KeyError, match="a1"):
+ self.client.sign_xs_common(cookies)
+
+ def test_sign_xsc_alias(self):
+ """测试 sign_xsc 别名方法"""
+ cookies = {
+ "a1": "test_a1_value",
+ "web_session": "test_session",
+ }
+
+ result = self.client.sign_xsc(cookies)
+
+ assert isinstance(result, str)
+ assert len(result) > 0
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add negative tests for `sign_xsc` / `validate_xs_common_params` to cover type and value validation
Since `sign_xsc` is decorated with `@validate_xs_common_params` (which calls `RequestSignatureValidator.validate_cookie`), the current test only covers the valid dict case. Please add negative tests that:
- Pass an unsupported type (e.g. list/int) as `cookie_dict` and assert a `TypeError` with the expected message.
- Use a cookie dict with non-string keys to cover the key-type validation path.
- Optionally, pass `None` (depending on the intended contract) and assert the expected behaviour.
This will exercise the validator’s error handling and lock in the parameter contract for `sign_xsc`.
Suggested implementation:
```python
+ def test_sign_xsc_alias(self):
+ """测试 sign_xsc 别名方法"""
+ cookies = {
+ "a1": "test_a1_value",
+ "web_session": "test_session",
+ }
+
+ result = self.client.sign_xsc(cookies)
+
+ assert isinstance(result, str)
+ assert len(result) > 0
+
+ def test_sign_xsc_invalid_type(self):
+ """测试 sign_xsc 传入非 dict 类型参数时抛出异常"""
+ # list 作为参数
+ with pytest.raises(TypeError, match="cookie_dict"):
+ self.client.sign_xsc(["not", "a", "dict"]) # type: ignore[arg-type]
+
+ # int 作为参数
+ with pytest.raises(TypeError, match="cookie_dict"):
+ self.client.sign_xsc(123) # type: ignore[arg-type]
+
+ def test_sign_xsc_non_string_keys(self):
+ """测试 sign_xsc 传入非字符串 key 的 cookie 时抛出异常"""
+ cookies = {
+ "a1": "test_a1_value",
+ "web_session": "test_session",
+ 123: "value_with_int_key", # 非字符串 key
+ }
+
+ with pytest.raises(TypeError, match="key"):
+ self.client.sign_xsc(cookies) # type: ignore[dict-item]
+
+ def test_sign_xsc_none_cookie_dict(self):
+ """测试 sign_xsc 传入 None 时的参数校验行为"""
+ with pytest.raises(TypeError, match="cookie_dict"):
+ self.client.sign_xsc(None) # type: ignore[arg-type]
+
+
```
These tests assume that `@validate_xs_common_params` / `RequestSignatureValidator.validate_cookie`:
1. Raise `TypeError` when `cookie_dict` is not a `dict` (list/int/None case) and that the error message contains `cookie_dict`.
2. Raise `TypeError` for non-string keys with an error message containing `key`.
If the actual validator uses different exception types or messages, adjust:
- The exception class in `pytest.raises(...)`.
- The `match` patterns to align with the real error messages.
No other code changes should be required if `self.client.sign_xsc` is already available in the test class.
</issue_to_address>
### Comment 9
<location> `tests/test_cookie_parsing.py:148-157` </location>
<code_context>
+class TestXsCommonSigner:
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen `XsCommonSigner` tests with a fixed-vector or structural assertion that covers the b1/Base64 regression
Current tests only verify that the output is non-empty and that missing `a1` raises `KeyError`, which doesn’t validate the corrected byte-based Base64 behaviour for `b1`.
To better cover this regression, please add a deterministic test that:
- Uses a fixed cookie dict and `CryptoConfig` (with deterministic fingerprint),
- Produces `b1` via `FingerprintGenerator.generate_b1()` or `XsCommonSigner`, and
- Asserts that `b1` (or `x-s-common`) matches a known-good value or prefix from reference browser/VM output (e.g. starts with `"I38r"`).
Suggested implementation:
```python
class TestXsCommonSigner:
"""测试 XsCommonSigner 类"""
def setup_method(self):
self.signer = XsCommonSigner()
def test_sign_with_dict_only(self):
"""测试只接受字典参数"""
cookies = {
"a1": "test_a1_value",
"web_session": "test_session",
}
# 基本行为:返回非空字符串
result = self.signer.sign(cookies)
assert isinstance(result, str)
assert result
def test_b1_base64_regression_vector(self):
"""使用固定向量验证 b1/Base64 行为,防止回归"""
cookies = {
"a1": "fixed_a1_value",
"web_session": "fixed_session_value",
}
# 使用固定的 CryptoConfig 以获得确定性的指纹
crypto_config = CryptoConfig(
fingerprint="fixed_fingerprint_for_test",
)
fp_gen = FingerprintGenerator(crypto_config=crypto_config)
# 直接生成 b1,确保是稳定的 Base64 字节编码
b1 = fp_gen.generate_b1(cookies)
assert isinstance(b1, str)
assert b1 # 非空
# 结构性断言:b1 必须以已知前缀开头(来自参考浏览器/VM 输出)
# 注意:前缀 "I38r" 需要根据实际参考输出进行同步
assert b1.startswith("I38r")
# 通过 XsCommonSigner 生成 x-s-common 头部,应与 b1 行为一致
x_s_common = self.signer.sign(cookies=cookies, crypto_config=crypto_config)
assert isinstance(x_s_common, str)
assert x_s_common
assert x_s_common.startswith("I38r")
```
1. 在 `tests/test_cookie_parsing.py` 顶部增加正确的导入(根据实际模块路径调整):
```python
from your_module_path import CryptoConfig, FingerprintGenerator
```
或者如果它们在不同模块中,请分别导入。
2. 确认 `XsCommonSigner` 的 API:
- 如果当前签名方法不是 `sign`,而是例如 `sign_xsc` 或其他名字,请将
```python
result = self.signer.sign(cookies)
x_s_common = self.signer.sign(cookies=cookies, crypto_config=crypto_config)
```
替换为实际的方法名和参数签名。
3. 如果 `CryptoConfig` 构造函数需要额外参数(例如密钥、盐值等),为测试构造一个完全确定性的配置(固定密钥/盐),并在 `test_b1_base64_regression_vector` 中补全这些参数。
4. `FingerprintGenerator` 的初始化和 `generate_b1` 调用可能需要不同的参数名或额外上下文:
- 将
```python
fp_gen = FingerprintGenerator(crypto_config=crypto_config)
b1 = fp_gen.generate_b1(cookies)
```
调整为当前项目中实际的初始化方式与方法签名。
5. `"I38r"` 仅是示例前缀,请从参考浏览器/VM 输出中获取真实的 `b1` 值或前缀,并更新断言:
```python
assert b1.startswith("<真实前缀>")
assert x_s_common.startswith("<真实前缀>")
```
6. 如项目中已有 pytest fixture 提供 `CryptoConfig` 或 `FingerprintGenerator`(例如 `crypto_config`、`fingerprint_generator`),可以将测试改写为使用这些 fixture,而不是在测试中直接构造实例,以保持与现有测试风格一致。
</issue_to_address>
### Comment 10
<location> `README.md:193` </location>
<code_context>
+cookie_string = "a1=your_a1_value; web_session=your_web_session; webId=your_web_id"
+xs_common = client.sign_xsc(cookie_dict=cookie_string)
+
+# 使用在请求中
+headers = {
+ "x-s-common": xs_common,
</code_context>
<issue_to_address>
**suggestion (typo):** “使用在请求中”建议调整为更自然的表达,如“在请求中使用”。
这句注释略显别扭,建议改为“# 在请求中使用”,更符合中文表达习惯。
```suggestion
# 在请求中使用
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| def encode(self, data_to_encode: bytes | str | Iterable[int]) -> str: | ||
| """ | ||
| Encode a string using custom Base64 alphabet | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): 新的 encode 函数签名与实现不一致,会在处理 bytes 和可迭代输入时出错。
新的类型标注允许 bytes 和 Iterable[int],但在非 bytearray 分支中实现仍然假定参数为 str。data_bytes = data_to_encode 之后再调用 data_to_encode.encode("utf-8"),对于 bytes 和通用可迭代对象都会失败。
要么让实现与注解保持一致(例如先把所有输入统一规范化为 bytes,再进行编码/转换),要么如果不需要更宽的支持,就把类型标注收窄回 str。
Original comment in English
issue (bug_risk): The new encode signature and implementation are inconsistent and will break for bytes and iterable inputs.
The new type hint allows bytes and Iterable[int], but the implementation still assumes a str in the non-bytearray branch. data_bytes = data_to_encode followed by data_to_encode.encode("utf-8") will fail for bytes and generic iterables.
Either align the implementation with the annotation (e.g. normalize all inputs to bytes first, then encode/translate) or narrow the annotation back to str if wider support isn’t required.
|
|
||
| c = cls.MASK32 | ||
|
|
||
| if isinstance(data, bytes | bytearray | memoryview): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): 使用 | union 的 isinstance 检查会在运行时抛异常,应该改为使用类型元组。
在 CPython 中,isinstance(obj, T1 | T2) 会抛出 TypeError: isinstance() argument 2 cannot be a union。为了在不引入运行时错误的前提下保持相同行为,应修改为:
if isinstance(data, (bytes, bytearray, memoryview)):
it = bytes(data)Original comment in English
issue (bug_risk): The isinstance check using a | union will raise at runtime; it should use a tuple of types instead.
In CPython, isinstance(obj, T1 | T2) raises TypeError: isinstance() argument 2 cannot be a union. To keep the same behavior without a runtime error, this should be:
if isinstance(data, (bytes, bytearray, memoryview)):
it = bytes(data)| @staticmethod | ||
| def validate_cookie(cookie: Any) -> dict[str, Any] | str: | ||
| """Validate cookie parameter""" | ||
| if cookie is not None and not (isinstance(cookie, dict) or isinstance(cookie, str)): | ||
| raise TypeError(f"payload must be dict or None, got {type(cookie).__name__}") | ||
| # detect cookie dict validation | ||
| if cookie is not None and isinstance(cookie, dict): | ||
| for key in cookie.keys(): | ||
| if not isinstance(key, str): | ||
| raise TypeError(f"payload keys must be str, got {type(key).__name__} for key '{key}'") | ||
| return cookie |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (bug_risk): validate_cookie 中的错误信息引用了 payload 而不是 cookie,并且可接受类型与装饰器的用法不匹配。
当前抛出的异常消息提到 payload,并声称只接受 dict or None,这对一个 cookie 校验函数来说具有误导性,也会让调试变得困难。
此外还存在类型不匹配:validate_xs_common_params 可以传入 None(通过默认值),但 validate_cookie 的返回类型注解不包含 None。建议在函数签名中允许 None,并对错误信息和类型进行对齐,例如:
def validate_cookie(cookie: Any) -> dict[str, Any] | str | None:
if cookie is not None and not isinstance(cookie, (dict, str)):
raise TypeError(f"cookie must be dict, str, or None, got {type(cookie).__name__}")
if isinstance(cookie, dict):
for key in cookie.keys():
if not isinstance(key, str):
raise TypeError(
f"cookie keys must be str, got {type(key).__name__} for key '{key}'"
)
return cookie| @staticmethod | |
| def validate_cookie(cookie: Any) -> dict[str, Any] | str: | |
| """Validate cookie parameter""" | |
| if cookie is not None and not (isinstance(cookie, dict) or isinstance(cookie, str)): | |
| raise TypeError(f"payload must be dict or None, got {type(cookie).__name__}") | |
| # detect cookie dict validation | |
| if cookie is not None and isinstance(cookie, dict): | |
| for key in cookie.keys(): | |
| if not isinstance(key, str): | |
| raise TypeError(f"payload keys must be str, got {type(key).__name__} for key '{key}'") | |
| return cookie | |
| @staticmethod | |
| def validate_cookie(cookie: Any) -> dict[str, Any] | str | None: | |
| """Validate cookie parameter""" | |
| if cookie is not None and not isinstance(cookie, (dict, str)): | |
| raise TypeError( | |
| f"cookie must be dict, str, or None, got {type(cookie).__name__}" | |
| ) | |
| if isinstance(cookie, dict): | |
| for key in cookie.keys(): | |
| if not isinstance(key, str): | |
| raise TypeError( | |
| f"cookie keys must be str, got {type(key).__name__} for key '{key}'" | |
| ) | |
| return cookie |
Original comment in English
suggestion (bug_risk): The error messages in validate_cookie reference payload instead of cookie and the accepted types don’t match the decorator usage.
The exceptions currently mention payload and refer to dict or None, which is misleading for a cookie validator and will confuse debugging.
There’s also a type mismatch: validate_xs_common_params can pass None (via the default), but validate_cookie’s return annotation doesn’t include None. Consider allowing None in the signature and aligning the error messages and types, e.g.:
def validate_cookie(cookie: Any) -> dict[str, Any] | str | None:
if cookie is not None and not isinstance(cookie, (dict, str)):
raise TypeError(f"cookie must be dict, str, or None, got {type(cookie).__name__}")
if isinstance(cookie, dict):
for key in cookie.keys():
if not isinstance(key, str):
raise TypeError(
f"cookie keys must be str, got {type(key).__name__} for key '{key}'"
)
return cookie| @staticmethod | |
| def validate_cookie(cookie: Any) -> dict[str, Any] | str: | |
| """Validate cookie parameter""" | |
| if cookie is not None and not (isinstance(cookie, dict) or isinstance(cookie, str)): | |
| raise TypeError(f"payload must be dict or None, got {type(cookie).__name__}") | |
| # detect cookie dict validation | |
| if cookie is not None and isinstance(cookie, dict): | |
| for key in cookie.keys(): | |
| if not isinstance(key, str): | |
| raise TypeError(f"payload keys must be str, got {type(key).__name__} for key '{key}'") | |
| return cookie | |
| @staticmethod | |
| def validate_cookie(cookie: Any) -> dict[str, Any] | str | None: | |
| """Validate cookie parameter""" | |
| if cookie is not None and not isinstance(cookie, (dict, str)): | |
| raise TypeError( | |
| f"cookie must be dict, str, or None, got {type(cookie).__name__}" | |
| ) | |
| if isinstance(cookie, dict): | |
| for key in cookie.keys(): | |
| if not isinstance(key, str): | |
| raise TypeError( | |
| f"cookie keys must be str, got {type(key).__name__} for key '{key}'" | |
| ) | |
| return cookie |
| def validate_xs_common_params(func: F) -> F: # type: ignore[misc] # noqa: UP047 | ||
| """ | ||
| Parameter validation decorator for the `sign_xsc` method. | ||
| This wrapper normalizes and validates the arguments before delegating to | ||
| the underlying signing implementation. | ||
| Args: | ||
| func: Method to be decorated. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): validate_xs_common_params 更改了 sign_xsc 的函数签名并允许传入 None,这会在下游实现中导致问题。
当前包装器将 sign_xsc 的契约从 dict[str, Any] | str 改成了 dict[str, Any] | None,并丢失了 str 这种调用形式。
这会引发两个问题:
- 传入字符串 cookie 的调用方不再受支持(或者类型标注变得不正确),因为按当前写法
validate_cookie并不接受str | None。 - 当
cookie_dict为None时,该值会一路传递到sign_xsc→_parse_cookies→XsCommonSigner.sign,而最后这个方法期望得到dict并执行cookie_dict["a1"],从而导致运行时抛出TypeError,而不是给出一致的校验错误。
为了解决这个问题,应保持包装器签名与 sign_xsc 一致,并确保 validate_cookie 支持 str 且对于必填参数拒绝 None:
def wrapper(self, cookie_dict: dict[str, Any] | str) -> str:
validator = RequestSignatureValidator()
validated_cookie = validator.validate_cookie(cookie_dict)
if validated_cookie is None:
raise ValueError("cookie_dict is required")
return func(self, validated_cookie)Original comment in English
issue (bug_risk): validate_xs_common_params changes the sign_xsc signature and allows None, which will break in the downstream implementation.
The wrapper currently changes the contract of sign_xsc from dict[str, Any] | str to dict[str, Any] | None and drops the str case.
This causes two problems:
- Callers passing a cookie string are no longer supported (or the types are wrong), since
validate_cookiedoesn’t acceptstr | Noneas written. - When
cookie_dictisNone,Nonepropagates intosign_xsc→_parse_cookies→XsCommonSigner.sign, which expects adictand doescookie_dict["a1"], leading to a runtimeTypeErrorinstead of consistent validation errors.
To fix this, keep the wrapper signature aligned with sign_xsc and ensure validate_cookie supports str and rejects None for required parameters:
def wrapper(self, cookie_dict: dict[str, Any] | str) -> str:
validator = RequestSignatureValidator()
validated_cookie = validator.validate_cookie(cookie_dict)
if validated_cookie is None:
raise ValueError("cookie_dict is required")
return func(self, validated_cookie)| ) | ||
|
|
||
| assert isinstance(headers, dict) | ||
| assert "x-s" in headers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (testing): 测试期望 sign_headers 输出中包含 x-s,但实现当前只返回 x-s-common。
Xhshow.sign_headers 的实现现在省略了 "x-s",只返回 "x-s-common"、"x-t"、"x-b3-traceid" 和 "x-xray-traceid",但这一处以及其它测试仍然断言存在 "x-s" 及其前缀。请先确定 sign_headers* 的对外契约究竟应该是什么,然后更新实现(重新暴露 "x-s")或更新测试(不再断言它),以保证两者保持一致。
Original comment in English
issue (testing): Tests expect x-s in sign_headers output but implementation currently only returns x-s-common
The implementation of Xhshow.sign_headers now omits "x-s" and only returns "x-s-common", "x-t", "x-b3-traceid", and "x-xray-traceid", but this and other tests still assert "x-s" and its prefix. Please decide what the contract of sign_headers* should be and update either the implementation (to re‑expose "x-s") or the tests (to stop asserting it) so they are consistent.
|
|
||
| assert isinstance(headers, dict) | ||
| assert all(k in headers for k in ["x-s", "x-t", "x-b3-traceid", "x-xray-traceid"]) | ||
| assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (testing): sign_headers_get/sign_headers_post 的测试仍然断言 x-s 的存在和格式,但该字段已从 headers 字典中移除。
这些测试仍然期望 "x-s" 存在并以 "XYS_" 开头,但 client.sign_headers 已不再返回这个 header。一旦确认期望的 header 集合,请只针对属于公开 API 的键进行断言。如果不再返回 x-s,请移除这里对 XYS_ 前缀的断言,或者把它移动到直接针对底层 sign_xs* 帮助函数的测试中。
Original comment in English
issue (testing): sign_headers_get/sign_headers_post tests still assert presence and format of x-s despite it being removed from the headers dict
These tests still expect "x-s" to be present and to start with "XYS_", but client.sign_headers no longer returns that header. Once the expected header set is confirmed, please update them to only assert keys that are part of the public API. If x-s is no longer returned, remove the XYS_ prefix assertion here or move it into a test that directly targets the lower-level sign_xs* helpers.
| ) | ||
|
|
||
| assert isinstance(headers, dict) | ||
| assert "x-s" in headers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (testing): 新增的 cookie/header 测试对 header 集合中的 x-s 进行了硬编码,这与当前实现不符。
这些测试目前断言 sign_headers* 的结果中存在 "x-s",并在某些情况下断言它以 "XYS_" 开头,但本 PR 已经将 sign_headers 修改为仅返回 x-s-common(以及 trace ID),不再返回 x-s。请根据预期 API 进行对齐:
- 如果
sign_headers*应该同时返回x-s和x-s-common,请在实现中恢复x-s。 - 如果只有
x-s-common是对外暴露的字段,请删除这里对x-s的断言,并把任何关于x-s格式的检查迁移到更底层的签名函数测试中。
Original comment in English
issue (testing): New cookie/header tests hard-code x-s in the header set, which conflicts with the current implementation
These tests currently assert that "x-s" is present in sign_headers* results and sometimes that it starts with "XYS_", but this PR changes sign_headers to return x-s-common (and trace IDs) without x-s. Please align the expectations with the intended API:
- If
sign_headers*should return bothx-sandx-s-common, restorex-sin the implementation. - If only
x-s-commonis public, drop thex-sassertions here and move anyx-sformat checks into lower-level signing tests.
| result = self.client.sign_xs_common(cookies) | ||
|
|
||
| assert isinstance(result, str) | ||
| assert len(result) > 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (testing): 为 sign_xsc / validate_xs_common_params 增加负向测试,覆盖类型和值校验逻辑。
由于 sign_xsc 使用了 @validate_xs_common_params 装饰器(内部会调用 RequestSignatureValidator.validate_cookie),当前测试只覆盖了传入合法字典的情况。请增加以下负向测试:
- 传入不受支持的类型(例如 list/int 作为
cookie_dict),并断言抛出带有预期消息的TypeError。 - 使用包含非字符串键的 cookie 字典,以覆盖键类型校验路径。
- (可选)传入
None(取决于预期契约),并断言预期的行为。
这将帮助验证校验器的错误处理逻辑,并固定 sign_xsc 的参数契约。
建议实现:
+ def test_sign_xsc_alias(self):
+ """测试 sign_xsc 别名方法"""
+ cookies = {
+ "a1": "test_a1_value",
+ "web_session": "test_session",
+ }
+
+ result = self.client.sign_xsc(cookies)
+
+ assert isinstance(result, str)
+ assert len(result) > 0
+
+ def test_sign_xsc_invalid_type(self):
+ """测试 sign_xsc 传入非 dict 类型参数时抛出异常"""
+ # list 作为参数
+ with pytest.raises(TypeError, match="cookie_dict"):
+ self.client.sign_xsc(["not", "a", "dict"]) # type: ignore[arg-type]
+
+ # int 作为参数
+ with pytest.raises(TypeError, match="cookie_dict"):
+ self.client.sign_xsc(123) # type: ignore[arg-type]
+
+ def test_sign_xsc_non_string_keys(self):
+ """测试 sign_xsc 传入非字符串 key 的 cookie 时抛出异常"""
+ cookies = {
+ "a1": "test_a1_value",
+ "web_session": "test_session",
+ 123: "value_with_int_key", # 非字符串 key
+ }
+
+ with pytest.raises(TypeError, match="key"):
+ self.client.sign_xsc(cookies) # type: ignore[dict-item]
+
+ def test_sign_xsc_none_cookie_dict(self):
+ """测试 sign_xsc 传入 None 时的参数校验行为"""
+ with pytest.raises(TypeError, match="cookie_dict"):
+ self.client.sign_xsc(None) # type: ignore[arg-type]
+
+这些测试假设 @validate_xs_common_params / RequestSignatureValidator.validate_cookie:
- 在
cookie_dict不是dict(list/int/None 情况)时抛出TypeError,并且错误信息包含cookie_dict。 - 在存在非字符串键时抛出
TypeError,并且错误信息包含key。
如果实际的校验器使用了不同的异常类型或消息,请相应地调整:
pytest.raises(...)中的异常类型;match模式,以与真实错误消息保持一致。
如果测试类中已经提供了 self.client.sign_xsc,则不需要额外的代码变更。
Original comment in English
suggestion (testing): Add negative tests for sign_xsc / validate_xs_common_params to cover type and value validation
Since sign_xsc is decorated with @validate_xs_common_params (which calls RequestSignatureValidator.validate_cookie), the current test only covers the valid dict case. Please add negative tests that:
- Pass an unsupported type (e.g. list/int) as
cookie_dictand assert aTypeErrorwith the expected message. - Use a cookie dict with non-string keys to cover the key-type validation path.
- Optionally, pass
None(depending on the intended contract) and assert the expected behaviour.
This will exercise the validator’s error handling and lock in the parameter contract for sign_xsc.
Suggested implementation:
+ def test_sign_xsc_alias(self):
+ """测试 sign_xsc 别名方法"""
+ cookies = {
+ "a1": "test_a1_value",
+ "web_session": "test_session",
+ }
+
+ result = self.client.sign_xsc(cookies)
+
+ assert isinstance(result, str)
+ assert len(result) > 0
+
+ def test_sign_xsc_invalid_type(self):
+ """测试 sign_xsc 传入非 dict 类型参数时抛出异常"""
+ # list 作为参数
+ with pytest.raises(TypeError, match="cookie_dict"):
+ self.client.sign_xsc(["not", "a", "dict"]) # type: ignore[arg-type]
+
+ # int 作为参数
+ with pytest.raises(TypeError, match="cookie_dict"):
+ self.client.sign_xsc(123) # type: ignore[arg-type]
+
+ def test_sign_xsc_non_string_keys(self):
+ """测试 sign_xsc 传入非字符串 key 的 cookie 时抛出异常"""
+ cookies = {
+ "a1": "test_a1_value",
+ "web_session": "test_session",
+ 123: "value_with_int_key", # 非字符串 key
+ }
+
+ with pytest.raises(TypeError, match="key"):
+ self.client.sign_xsc(cookies) # type: ignore[dict-item]
+
+ def test_sign_xsc_none_cookie_dict(self):
+ """测试 sign_xsc 传入 None 时的参数校验行为"""
+ with pytest.raises(TypeError, match="cookie_dict"):
+ self.client.sign_xsc(None) # type: ignore[arg-type]
+
+These tests assume that @validate_xs_common_params / RequestSignatureValidator.validate_cookie:
- Raise
TypeErrorwhencookie_dictis not adict(list/int/None case) and that the error message containscookie_dict. - Raise
TypeErrorfor non-string keys with an error message containingkey.
If the actual validator uses different exception types or messages, adjust:
- The exception class in
pytest.raises(...). - The
matchpatterns to align with the real error messages.
No other code changes should be required if self.client.sign_xsc is already available in the test class.
| class TestXsCommonSigner: | ||
| """测试 XsCommonSigner 类""" | ||
|
|
||
| def setup_method(self): | ||
| self.signer = XsCommonSigner() | ||
|
|
||
| def test_sign_with_dict_only(self): | ||
| """测试只接受字典参数""" | ||
| cookies = { | ||
| "a1": "test_a1_value", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (testing): 强化 XsCommonSigner 的测试,用固定向量或结构性断言覆盖 b1/Base64 回归问题。
当前测试只验证输出非空以及缺少 a1 时是否抛出 KeyError,并没有真正验证修复后的基于字节的 b1 Base64 行为。
为了更好地覆盖此次回归修复,建议增加一个确定性的测试:
- 使用固定的 cookie 字典和
CryptoConfig(具有确定性的 fingerprint), - 通过
FingerprintGenerator.generate_b1()或XsCommonSigner生成b1, - 断言
b1(或x-s-common)与参考浏览器/VM 输出的已知正确值或前缀匹配(例如以"I38r"开头)。
建议实现:
class TestXsCommonSigner:
"""测试 XsCommonSigner 类"""
def setup_method(self):
self.signer = XsCommonSigner()
def test_sign_with_dict_only(self):
"""测试只接受字典参数"""
cookies = {
"a1": "test_a1_value",
"web_session": "test_session",
}
# 基本行为:返回非空字符串
result = self.signer.sign(cookies)
assert isinstance(result, str)
assert result
def test_b1_base64_regression_vector(self):
"""使用固定向量验证 b1/Base64 行为,防止回归"""
cookies = {
"a1": "fixed_a1_value",
"web_session": "fixed_session_value",
}
# 使用固定的 CryptoConfig 以获得确定性的指纹
crypto_config = CryptoConfig(
fingerprint="fixed_fingerprint_for_test",
)
fp_gen = FingerprintGenerator(crypto_config=crypto_config)
# 直接生成 b1,确保是稳定的 Base64 字节编码
b1 = fp_gen.generate_b1(cookies)
assert isinstance(b1, str)
assert b1 # 非空
# 结构性断言:b1 必须以已知前缀开头(来自参考浏览器/VM 输出)
# 注意:前缀 "I38r" 需要根据实际参考输出进行同步
assert b1.startswith("I38r")
# 通过 XsCommonSigner 生成 x-s-common 头部,应与 b1 行为一致
x_s_common = self.signer.sign(cookies=cookies, crypto_config=crypto_config)
assert isinstance(x_s_common, str)
assert x_s_common
assert x_s_common.startswith("I38r")-
在
tests/test_cookie_parsing.py顶部增加正确的导入(根据实际模块路径调整):from your_module_path import CryptoConfig, FingerprintGenerator
或者如果它们在不同模块中,请分别导入。
-
确认
XsCommonSigner的 API:- 如果当前签名方法不是
sign,而是例如sign_xsc或其他名字,请将替换为实际的方法名和参数签名。result = self.signer.sign(cookies) x_s_common = self.signer.sign(cookies=cookies, crypto_config=crypto_config)
- 如果当前签名方法不是
-
如果
CryptoConfig构造函数需要额外参数(例如密钥、盐值等),为测试构造一个完全确定性的配置(固定密钥/盐),并在test_b1_base64_regression_vector中补全这些参数。 -
FingerprintGenerator的初始化和generate_b1调用可能需要不同的参数名或额外上下文:- 将
调整为当前项目中实际的初始化方式与方法签名。
fp_gen = FingerprintGenerator(crypto_config=crypto_config) b1 = fp_gen.generate_b1(cookies)
- 将
-
"I38r"仅是示例前缀,请从参考浏览器/VM 输出中获取真实的b1值或前缀,并更新断言:assert b1.startswith("<真实前缀>") assert x_s_common.startswith("<真实前缀>")
-
如项目中已有 pytest fixture 提供
CryptoConfig或FingerprintGenerator(例如crypto_config、fingerprint_generator),可以将测试改写为使用这些 fixture,而不是在测试中直接构造实例,以保持与现有测试风格一致。
Original comment in English
suggestion (testing): Strengthen XsCommonSigner tests with a fixed-vector or structural assertion that covers the b1/Base64 regression
Current tests only verify that the output is non-empty and that missing a1 raises KeyError, which doesn’t validate the corrected byte-based Base64 behaviour for b1.
To better cover this regression, please add a deterministic test that:
- Uses a fixed cookie dict and
CryptoConfig(with deterministic fingerprint), - Produces
b1viaFingerprintGenerator.generate_b1()orXsCommonSigner, and - Asserts that
b1(orx-s-common) matches a known-good value or prefix from reference browser/VM output (e.g. starts with"I38r").
Suggested implementation:
class TestXsCommonSigner:
"""测试 XsCommonSigner 类"""
def setup_method(self):
self.signer = XsCommonSigner()
def test_sign_with_dict_only(self):
"""测试只接受字典参数"""
cookies = {
"a1": "test_a1_value",
"web_session": "test_session",
}
# 基本行为:返回非空字符串
result = self.signer.sign(cookies)
assert isinstance(result, str)
assert result
def test_b1_base64_regression_vector(self):
"""使用固定向量验证 b1/Base64 行为,防止回归"""
cookies = {
"a1": "fixed_a1_value",
"web_session": "fixed_session_value",
}
# 使用固定的 CryptoConfig 以获得确定性的指纹
crypto_config = CryptoConfig(
fingerprint="fixed_fingerprint_for_test",
)
fp_gen = FingerprintGenerator(crypto_config=crypto_config)
# 直接生成 b1,确保是稳定的 Base64 字节编码
b1 = fp_gen.generate_b1(cookies)
assert isinstance(b1, str)
assert b1 # 非空
# 结构性断言:b1 必须以已知前缀开头(来自参考浏览器/VM 输出)
# 注意:前缀 "I38r" 需要根据实际参考输出进行同步
assert b1.startswith("I38r")
# 通过 XsCommonSigner 生成 x-s-common 头部,应与 b1 行为一致
x_s_common = self.signer.sign(cookies=cookies, crypto_config=crypto_config)
assert isinstance(x_s_common, str)
assert x_s_common
assert x_s_common.startswith("I38r")-
在
tests/test_cookie_parsing.py顶部增加正确的导入(根据实际模块路径调整):from your_module_path import CryptoConfig, FingerprintGenerator
或者如果它们在不同模块中,请分别导入。
-
确认
XsCommonSigner的 API:- 如果当前签名方法不是
sign,而是例如sign_xsc或其他名字,请将替换为实际的方法名和参数签名。result = self.signer.sign(cookies) x_s_common = self.signer.sign(cookies=cookies, crypto_config=crypto_config)
- 如果当前签名方法不是
-
如果
CryptoConfig构造函数需要额外参数(例如密钥、盐值等),为测试构造一个完全确定性的配置(固定密钥/盐),并在test_b1_base64_regression_vector中补全这些参数。 -
FingerprintGenerator的初始化和generate_b1调用可能需要不同的参数名或额外上下文:- 将
调整为当前项目中实际的初始化方式与方法签名。
fp_gen = FingerprintGenerator(crypto_config=crypto_config) b1 = fp_gen.generate_b1(cookies)
- 将
-
"I38r"仅是示例前缀,请从参考浏览器/VM 输出中获取真实的b1值或前缀,并更新断言:assert b1.startswith("<真实前缀>") assert x_s_common.startswith("<真实前缀>")
-
如项目中已有 pytest fixture 提供
CryptoConfig或FingerprintGenerator(例如crypto_config、fingerprint_generator),可以将测试改写为使用这些 fixture,而不是在测试中直接构造实例,以保持与现有测试风格一致。
| cookie_string = "a1=your_a1_value; web_session=your_web_session; webId=your_web_id" | ||
| xs_common = client.sign_xsc(cookie_dict=cookie_string) | ||
|
|
||
| # 使用在请求中 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (typo): “使用在请求中”建议调整为更自然的表达,如“在请求中使用”。
这句注释略显别扭,建议改为“# 在请求中使用”,更符合中文表达习惯。
| # 使用在请求中 | |
| # 在请求中使用 |
Original comment in English
suggestion (typo): “使用在请求中”建议调整为更自然的表达,如“在请求中使用”。
这句注释略显别扭,建议改为“# 在请求中使用”,更符合中文表达习惯。
| # 使用在请求中 | |
| # 在请求中使用 |
Bug fix
Encode b1 from raw bytes (bytearray) instead of string/text to avoid byte corruption during encoding.
This restores parity with browser/VM output: the base64 result must start with I38r….
Prevents downstream requests from being rejected due to an invalid b1 value.
Sourcery 总结
基于完整的 cookies 和类浏览器指纹添加 x-s-common 签名流程,并将其与现有的 x-s 支持一起接入到 header 签名中。
新功能:
XsCommonSigner以及客户端方法sign_xs_common和其别名sign_xsc暴露 x-s-common 签名能力,支持传入完整的 cookie 字典或 cookie 字符串。b1值。错误修复:
b1值损坏,并恢复与浏览器/VM 输出的一致性。增强改进:
Base64Encoder,加入缓存的转换表并支持更广泛的输入类型,在自定义字母表和 x3 编解码场景中提升性能与灵活性。CryptoConfig,加入更多加密常量,包括与b1相关的密钥、x-s-common 模板以及更新后的 X3 前缀。x-s和x-s-commonheaders,并对a1等必需字段进行校验。构建:
curl-cffi和pycryptodome的新运行时依赖。文档:
sign_headers*、sign_xs*和sign_xscAPI 的职责分离。测试:
Original summary in English
Summary by Sourcery
Add x-s-common signing pipeline based on full cookies and browser-like fingerprints, and wire it into header signing alongside existing x-s support.
New Features:
Bug Fixes:
Enhancements:
Build:
Documentation:
Tests:
新功能:
XsCommonSigner。sign_xs_common及其别名sign_xsc,用于从 cookie 字典或 cookie 字符串生成 x-s-common 值,并将其接入sign_headers*,后者现在接受完整 cookies 而不仅仅是a1。错误修复:
增强:
Base64Encoder,加入缓存翻译表以及更广泛的输入类型支持,在自定义字母表场景下提升性能和灵活性。CryptoConfig,补充更多与密码学和签名相关的常量,包括更新的 X3 前缀以及 b1/x-s-common 配置。构建:
curl-cffi和pycryptodome的新运行时依赖。文档:
sign_headers*与sign_xs*API 之间的职责划分。测试:
Original summary in English
Sourcery 总结
基于完整的 cookies 和类浏览器指纹添加 x-s-common 签名流程,并将其与现有的 x-s 支持一起接入到 header 签名中。
新功能:
XsCommonSigner以及客户端方法sign_xs_common和其别名sign_xsc暴露 x-s-common 签名能力,支持传入完整的 cookie 字典或 cookie 字符串。b1值。错误修复:
b1值损坏,并恢复与浏览器/VM 输出的一致性。增强改进:
Base64Encoder,加入缓存的转换表并支持更广泛的输入类型,在自定义字母表和 x3 编解码场景中提升性能与灵活性。CryptoConfig,加入更多加密常量,包括与b1相关的密钥、x-s-common 模板以及更新后的 X3 前缀。x-s和x-s-commonheaders,并对a1等必需字段进行校验。构建:
curl-cffi和pycryptodome的新运行时依赖。文档:
sign_headers*、sign_xs*和sign_xscAPI 的职责分离。测试:
Original summary in English
Summary by Sourcery
Add x-s-common signing pipeline based on full cookies and browser-like fingerprints, and wire it into header signing alongside existing x-s support.
New Features:
Bug Fixes:
Enhancements:
Build:
Documentation:
Tests: